package ro.dta.idbi.model;

import java.io.IOException;
import java.lang.reflect.Constructor;
import java.util.ArrayList;
import java.util.EnumSet;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Matcher;

import ro.dta.idbi.interfaces.IntegrationOperation;
import uk.ac.ic.doc.automed.IntegrityException;
import uk.ac.ic.doc.automed.NotFoundException;
import uk.ac.ic.doc.automed.modelmanagement.modeldef.SQLModelDef;
import uk.ac.ic.doc.automed.reps.Construct;
import uk.ac.ic.doc.automed.reps.Model;
import uk.ac.ic.doc.automed.reps.Schema;
import uk.ac.ic.doc.automed.reps.TypeMismatchException;

/**
 * Abstract class for transformation patterns using the BAV primitive
 * transformations
 * 
 * @author Tudor Dobrila
 * 
 */
public abstract class AbstractPattern {

	/**
	 * Regular expression for a comma, preceeded or followed by any number of
	 * spaces
	 */
	public static final String SPACE_REGEX = "[ ]*";

	/**
	 * Regular expression for a comma, preceeded or followed by any number of
	 * spaces
	 */
	public static final String COMMA_REGEX = SPACE_REGEX + "," + SPACE_REGEX;

	/**
	 * Regular expression for a name
	 */
	public static final String NAME_REGEX = "[^ ,\\[\\]\\{\\}]+";

	/**
	 * Regular expression for a table name
	 */
	public static final String TABLE_REGEX = "<<" + NAME_REGEX + ">>";

	/**
	 * Regular expression for the definition of a table i.e. name + extent
	 */
	public static final String TABLE_DEF_REGEX = "table:(" + TABLE_REGEX + ")";

	/**
	 * Regular expression for a column
	 */
	public static final String COL_REGEX = "<<" + NAME_REGEX + "," + NAME_REGEX + ">>";

	/**
	 * Regular expression for the full definition of a column
	 */
	public static final String COL_DEF_REGEX = "column:<<(" + NAME_REGEX + "),(" + NAME_REGEX
			+ "),(" + NAME_REGEX + "),(" + NAME_REGEX + ")>>";

	/**
	 * Regular expression for a sequence of values
	 */
	public static final String VAL_SEQ_REGEX = "((\\{[ ]*(" + NAME_REGEX + COMMA_REGEX + ")*"
			+ NAME_REGEX + "[ ]*\\})|(" + NAME_REGEX + "))";

	/**
	 * Regular expression for a sequence of columns
	 */
	public static final String COL_SEQ_REGEX = "(([ ]*(" + COL_REGEX + COMMA_REGEX + ")*"
			+ COL_REGEX + "[ ]*)|(" + COL_REGEX + "))";

	/**
	 * Regular expression for a sequence of tables
	 */
	public static final String TABLE_SEQ_REGEX = "(([ ]*(" + TABLE_REGEX + COMMA_REGEX + ")*"
			+ TABLE_REGEX + "[ ]*)|(" + TABLE_REGEX + "))";

	/**
	 * Regular expression for the name of a table followed by a list of columns
	 */
	public static final String TABLE_AND_COLS_REGEX = "(" + NAME_REGEX + ")" + COMMA_REGEX
			+ COL_SEQ_REGEX;

	/**
	 * Regular expression for the definition of a primary key
	 */
	public static final String PK_DEF_REGEX = "primarykey:(<<(" + NAME_REGEX + ")" + COMMA_REGEX
			+ TABLE_AND_COLS_REGEX + ">>)";

	/**
	 * Regular expresion for the definition of a foreign key
	 */
	public static final String FK_DEF_REGEX = "foreignkey:(<<(" + NAME_REGEX + ")" + COMMA_REGEX
			+ TABLE_AND_COLS_REGEX + COMMA_REGEX + TABLE_AND_COLS_REGEX + ">>)";

	/**
	 * Command parse pattern
	 */
	protected transient java.util.regex.Pattern pattern;

	/**
	 * Source schema
	 */
	protected transient Schema sourceSchema;

	/**
	 * Table construct
	 */
	public static Construct table;

	/**
	 * Column construct
	 */
	public static Construct column;

	/**
	 * Primary Key construct
	 */
	public static Construct primaryKey;

	/**
	 * Foreign key construct
	 */
	public static Construct foreignKey;

	/**
	 * Table construct in the source_sql_* model
	 */
	public static Construct srcTable;

	/**
	 * Primary key construct in the source_sql_* model
	 */
	public static Construct srcPrimaryKey;

	/**
	 * Foreign key construct in the source_sql_* model
	 */
	public static Construct srcForeignKey;

	/**
	 * Sql model definition in the AutoMed repository
	 */
	protected static SQLModelDef sqlModel;

	/**
	 * Sql 92 model
	 */
	protected static Model sql92Model;

	static {
		// Initialise the SQL model
		final Logger logger = Logger.getLogger("ro.dta.idbi.model.AbstractPattern");
		sqlModel = new SQLModelDef();
		try {
			sqlModel.setFeatures(30);

			final String autoMedModelName = sqlModel.getAutoMedModelName();
			sql92Model = Model.getModel(autoMedModelName);
			table = sql92Model.getConstruct(SQLModelDef.AUTOMED_SQL_MODEL_TABLE);
			column = sql92Model.getConstruct(SQLModelDef.AUTOMED_SQL_MODEL_COLUMN);

			if (sqlModel.isFeatureInUse(SQLModelDef.PRIMARY_KEY)) {
				primaryKey = sql92Model.getConstruct(SQLModelDef.AUTOMED_SQL_MODEL_PRIMARY_KEY);
			}
			if (sqlModel.isFeatureInUse(SQLModelDef.FOREIGN_KEY)) {
				foreignKey = sql92Model.getConstruct(SQLModelDef.AUTOMED_SQL_MODEL_FOREIGN_KEY);
			}

			if (Model.exists("source_sql_30")) {
				Model srcModel = Model.getModel("source_sql_30");
				srcTable = srcModel.getConstruct("tuple_sql_table");
				if (sqlModel.isFeatureInUse(SQLModelDef.PRIMARY_KEY)) {
					srcPrimaryKey = srcModel.getConstruct("tuple_sql_primary_key");
				}
				if (sqlModel.isFeatureInUse(SQLModelDef.FOREIGN_KEY)) {
					srcForeignKey = srcModel.getConstruct("tuple_sql_foreign_key");
				}
			}
		} catch (IntegrityException e) {
			logger.log(Level.SEVERE, "Fatal exception", e);
		} catch (NotFoundException e) {
			logger.log(Level.SEVERE, "Fatal exception", e);
		}
	}

	/**
	 * Transformation pattern constructor
	 * 
	 * @param sourceSchema
	 *            Source schema on which the transformation will be applied
	 * @throws NotFoundException
	 *             Thrown if source schema does not exist
	 * @throws IntegrityException
	 *             Thrown if the SQL model definition could not be read
	 * 
	 */
	public AbstractPattern(final Schema sourceSchema) {
		this.sourceSchema = sourceSchema;
	}

	/**
	 * Transformation pattern constructor
	 * 
	 * @param sourceSchema
	 *            Name of source schema on which the transformation will be
	 *            applied
	 * @throws NotFoundException
	 *             Thrown if source schema does not exist
	 * @throws IntegrityException
	 *             Thrown if the SQL model definition could not be read
	 * 
	 */
	public AbstractPattern(final String sourceSchema) throws NotFoundException, IntegrityException {
		this(Schema.getSchema(sourceSchema));
	}

	/**
	 * Check a transformation command
	 * 
	 * @param trans
	 *            Transformation command
	 * @return True if command is valid, false otherwise
	 */
	public boolean check(final String trans) {
		final Matcher matcher = pattern.matcher(trans);
		return matcher.matches();
	}

	/**
	 * Parse a transformation command
	 * 
	 * @param trans
	 *            Transformation command
	 * @return Schema after the pattern has been applied on it
	 * @throws TransformationException
	 *             If an error occured during the transformation
	 */
	public Schema execute(final String trans) throws NotFoundException, TypeMismatchException,
			IntegrityException, TransformationException {
		if (!check(trans)) {
			throw new IllegalArgumentException("Command is invalid.");
		}
		this.parse(trans);

		if (!verify()) {
			throw new IllegalArgumentException("Can't apply this transformation.");
		}

		Schema crtSchema = sourceSchema;
		List<String> commands = getPrimitiveCommands();
		for (String cmd : commands) {
			try {
				crtSchema = AbstractPattern.execute(crtSchema, cmd);
			} catch (Exception e) {
				throw new TransformationException(e);
			}
		}

		return crtSchema;
	}

	/**
	 * Parse a transformation macro, to obtain the elements that will be
	 * transformed
	 * 
	 * @param trans
	 *            Transformation command
	 */
	protected abstract void parse(String trans);

	/**
	 * Get the primitive commands associated with the transformation pattern
	 * 
	 * @param trans
	 *            Transformation command
	 * @return Primitive commands associated with the transformation pattern
	 */
	protected List<String> getPrimitiveCommands(String trans) throws NotFoundException,
			IntegrityException {
		if (!check(trans)) {
			throw new IllegalArgumentException("Command is invalid.");
		}
		this.parse(trans);
		return getPrimitiveCommands();
	}

	/**
	 * Get the primitive commands associated with the current transformation
	 * pattern
	 * 
	 * @return Primitive commands associated with the current transformation
	 *         pattern
	 * @throws NotFoundException
	 * @throws IntegrityException
	 */
	protected abstract List<String> getPrimitiveCommands() throws NotFoundException,
			IntegrityException;

	/**
	 * Check if the transformation can be applied
	 * 
	 * @return True if transformation pattern can be applied, false otherwise
	 */
	protected abstract boolean verify();

	/**
	 * @return the sourceSchema
	 */
	public Schema getSourceSchema() {
		return sourceSchema;
	}

	/**
	 * Execute a command on a given schema
	 * 
	 * @param schema
	 *            Schema on which to execute the command
	 * @param command
	 *            Command to execute on schema
	 * @return Resulting schema after the transformation has been applied
	 * @throws TransformationException
	 *             If an error occured during the application of the
	 *             transformation
	 */
	public static Schema execute(final Schema schema, String command)
			throws TransformationException {
		Schema result = null;
		final int bracket = command.trim().indexOf('(');
		if (bracket != -1) {
			try {
				// TODO move this to the command macro field
				command = command.replace("?", "");
				final String name = command.substring(0, bracket).trim();
				final Pattern ptrn = getPattern(name);

				if (ptrn != null) {
					final Constructor<?> constructor = ptrn.getPatternClass().getConstructor(
							Schema.class);
					final AbstractPattern pattern = (AbstractPattern) constructor
							.newInstance(schema);
					result = pattern.execute(command);
				}
			} catch (Exception e) {
				throw new TransformationException(e);
			}
		}

		return result;
	}

	/**
	 * Execute a sequence of commands in order
	 * 
	 * @param schema
	 *            Initial schema over which to apply the sequence of
	 *            transformations
	 * @param commands
	 *            List of commands to execute
	 * @return Final schema, after the execution of all commands
	 * @throws TransformationException
	 *             If an exception occured during the execution
	 */
	public static Schema execute(final Schema schema, final String[] commands)
			throws TransformationException {
		Schema crtSchema = schema;
		for (String c : commands) {
			crtSchema = execute(crtSchema, c);
		}

		return crtSchema;
	}

	/**
	 * Get the pattern associated with a given command
	 * 
	 * @param command
	 *            Command to look for
	 * @return Pattern associated with the command
	 */
	public static Pattern getPattern(final String command) throws ClassNotFoundException,
			IOException {
		final EnumSet<IntegrationOperation> all = EnumSet.allOf(IntegrationOperation.class);
		final List<Pattern> patterns = Utils.getPatterns(all);
		Pattern result = null;
		for (Pattern p : patterns) {
			if (p.getCommand().equals(command)) {
				result = p;
				break;
			}
		}

		return result;
	}

	/**
	 * Parse the name of an column into a key/value pair, where the key
	 * represents the name of the table holding the column and value represents
	 * the name of the column
	 * 
	 * @param name
	 *            Name of the column to parse
	 * @return Key/value pair, where the key represents the name of the table
	 *         holding the column and value represents the name of the column
	 */
	public static Map.Entry<String, String> parseColName(final String name) {
		final String nameMod = name.substring(2, name.length() - 2);
		final String[] tokens = nameMod.split(",");

		if (tokens.length == 1) {
			throw new IllegalArgumentException(name + " does not represent a valid column name.");
		}

		final Map.Entry<String, String> result = new Map.Entry<String, String>() {

			@Override
			public String setValue(final String value) {
				return null;
			}

			@Override
			public String getValue() {
				if (tokens.length > 1) {
					return tokens[1].trim();
				} else {
					return tokens[0].trim();
				}
			}

			@Override
			public String getKey() {
				return tokens[0].trim();
			}
		};

		return result;
	}

	/**
	 * Remove the enclosing &lt; and &gt; signs from the name of a table
	 * 
	 * @param name
	 *            Name of the table in AutoMed format
	 * @return Name of the table, without enclosing &lt; and &gt; symbols
	 */
	public static String parseTableName(final String name) {
		return name.substring(2, name.length() - 2);
	}

	/**
	 * Parse the columns set from a string
	 * 
	 * @param valuesList
	 *            Columns set as string
	 * @return ArrayList of columns
	 */
	public static List<String> splitValues(final String valuesList) {
		final List<String> result = new ArrayList<String>();
		final String[] tokens = valuesList.split(",");

		for (String s1 : tokens) {
			result.add(s1.replace("{", "").replace("}", "").replace("?", "").trim());
		}

		return result;
	}

	/**
	 * Get the list of columns as a List from a list expressed as a String
	 * 
	 * @param colList
	 *            List of columns expressed as a String
	 * @return List of columns expressed as a List
	 */
	public static List<String> getColumns(String colList) {
		String[] tokens = colList.split(">>[ ]*,[ ]*<<");
		List<String> cols = new ArrayList<String>();
		if (tokens.length > 1) {
			cols.add(tokens[0] + ">>");
			for (int i = 1; i < tokens.length - 1; i++) {
				cols.add("<<" + tokens[i] + ">>");
			}
			cols.add("<<" + tokens[tokens.length - 1]);
		} else {
			cols.add(colList);
		}

		return cols;
	}
}
